/**
 * @ignore
 * 
 * @author 		Zachary Forest Johnson (indiemaps.com/blog)
 * 
 * @license 	Released under the New BSD License.
 */
package com.indiemaps.mapping.data.parsers.e00
{
	import __AS3__.vec.Vector;
	
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.Dictionary;
	
	/**
	 * Includes methods for performing some common and uncommon tasks with E00 topological GIS data. */
	public class E00Tools
	{
		public function E00Tools()
		{
		}
		
		/**
		 * E00 files store polygons, arcs, and nodes, and their topological relationships.
		 * The file format does not specifically separate out features.  
		 * For example, the U.S. state of Florida may be stored as multiple polygons, much like rings in a polygonal shapefile.
		 * This function returns a dictionary of features, with each feature's unique attribute value as the key to the dictionary. */
		public static function getAllFeaturePolys( dataSection : Vector.<Object>, attribute : String ) : Dictionary
		{
			var features : Dictionary = new Dictionary();
			var value : Object;
			
			// determine id field
			var idField : String;
			for ( var field : String in dataSection[ 0 ] ) {
				if ( field.substr( -2, 2 ) == 'ID' ) {
					idField = field;
					break;
				}
			}
			
			for each ( var poly : Object in dataSection ) {
				value = poly[ attribute ];
				if ( features[ value ] ) {
					( features[ value ][ 'polygons' ] as Array ).push( Number( poly[ idField ] ) );
				} else {
					features[ value ] = new Object();
					features[ value ][ 'polygons' ] = [ Number( poly[ idField ] ) ];
				}
			}
			
			return features;
		}
		

		/**
		 * Returns the centroids of the largest polygon in each feature
		 */
		public static function getAllFeatureCentroids(
			e00Data : Object,
			dataSection : Vector.<Object>,
			attribute : String,
			conversionFunction : Function = null
		) : Dictionary
		{
			var featureCentroids : Dictionary = new Dictionary();
			
			var features : Dictionary = getAllFeaturePolys( dataSection, attribute );
			
			for ( var key : Object in features ) {
				var feature : Object = features[ key ];
				var largestPolyID : int;
				var largestPolyArea : Number = -Infinity;
				
				for each ( var polyID : int in feature[ 'polygons' ] ) {
					var polyArea : Number = getPolygonArea( e00Data, polyID, conversionFunction );
					if ( polyArea > largestPolyArea ) {
						largestPolyArea = polyArea;
						largestPolyID = polyID;
					}
				}
				featureCentroids[ key ] = getPolygonCentroid( e00Data, largestPolyID, largestPolyArea, conversionFunction );
			}
			
			return featureCentroids;
		}
		
		private static function getPolygonArea(
			e00Data : Object,
			polyID : int,
			conversionFunction : Function = null
		) : Number
		{
			var area : Number = 0;
			var j : int;
			var iPoint : Object;
			var jPoint : Object;
			for each ( var arc : Object in e00Data[ 'pal' ][ polyID ][ 'points' ] ) {
				var arcNum : int = Math.abs( arc[ 'arc-number' ] ) - 1;
				if ( arcNum == -1 ) {
					continue;
				}
				if ( arc[ 'arc-number' ] < 0 ) {
					for ( var pointNum : int = e00Data[ 'arc' ][ arcNum ][ 'coord' ][ 'length' ]-1; pointNum >= 0; pointNum -- ) {
						var point : Object = ( conversionFunction == null ) ? e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] : conversionFunction( e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] );
						if ( ! iPoint ) {
							iPoint = point;
							continue;
						}
						jPoint = point;
						area += iPoint.x * jPoint.y;
						area -= iPoint.y * jPoint.x;
						iPoint = jPoint;
					}
				} else {
					for ( pointNum = 0; pointNum < e00Data[ 'arc' ][ arcNum ][ 'coord' ][ 'length' ]; pointNum ++ ) {
						point = ( conversionFunction == null ) ? e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] : conversionFunction( e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] );
						if ( ! iPoint ) {
							iPoint = point;
							continue;
						}
						jPoint = point;
						area += iPoint.x * jPoint.y;
						area -= iPoint.y * jPoint.x;
						iPoint = jPoint;
					}
				}
			}
			return Math.abs( area / 2 );
		}
		
		private static function getPolygonCentroid(
			e00Data : Object,
			polyID : int,
			polyArea : Number = NaN,
			conversionFunction : Function = null
		) : Point
		{
			if ( isNaN( polyArea ) ) polyArea = getPolygonArea( e00Data, polyID, conversionFunction );
			var center : Point = new Point();
			var iPoint : Object;
			var jPoint : Object;
			
			for ( var arcI : int = e00Data[ 'pal' ][ polyID ][ 'points' ][ 'length' ] - 1; arcI >= 0; arcI -- ) {
				var arc : Object = e00Data[ 'pal' ][ polyID ][ 'points' ][ arcI ];
				var arcNum : int = Math.abs( arc[ 'arc-number' ] ) - 1;
				if ( arcNum == -1 ) {
					continue;
				}
				if ( arc[ 'arc-number' ] > 0 ) {
					for ( var pointNum : int = e00Data[ 'arc' ][ arcNum ][ 'coord' ][ 'length' ]-1; pointNum >= 0; pointNum -- ) {
						var point : Object = ( conversionFunction == null ) ? e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] : conversionFunction( e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] );
						if ( ! iPoint ) {
							iPoint = point;
							continue;
						}
						
						jPoint = point;
						center.x += ( iPoint.x + jPoint.x ) * ( ( iPoint.x * jPoint.y ) - ( jPoint.x * iPoint.y ) );
						center.y += ( iPoint.y + jPoint.y ) * ( ( iPoint.x * jPoint.y ) - ( jPoint.x * iPoint.y ) );
						iPoint = jPoint;
					}
				} else {
					for ( pointNum = 0; pointNum < e00Data[ 'arc' ][ arcNum ][ 'coord' ][ 'length' ]; pointNum ++ ) {
						point = ( conversionFunction == null ) ? e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] : conversionFunction( e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] );
						if ( ! iPoint ) {
							iPoint = point;
							continue;
						}
						
						jPoint = point;
						center.x += ( iPoint.x + jPoint.x ) * ( ( iPoint.x * jPoint.y ) - ( jPoint.x * iPoint.y ) );
						center.y += ( iPoint.y + jPoint.y ) * ( ( iPoint.x * jPoint.y ) - ( jPoint.x * iPoint.y ) );
						iPoint = jPoint;
					}
				}
			}
			
			center.x /= ( 6 * polyArea );
			center.y /= ( 6 * polyArea );
			return center;
		}
		
		/**
		 * Returns the shared borders for every feature and its neighbors.
		 * Shared border lengths are in map/screen units, and a conversion function may be passed in for map projections. */
		public static function getAllFeatureSharedBorderLengths(
			e00Data : Object,
			dataSection : Vector.<Object>,
			attribute : String,
			zoom : Number = 1,
			conversionFunction : Function = null
		) : Dictionary
		{
			var featureBorders : Dictionary = new Dictionary();
			
			var features : Dictionary = getAllFeaturePolys( dataSection, attribute );
			for each ( var arc : Object in e00Data[ 'arc' ] ) {
				
				// do we give a shit?
				if ( ! sameFeature( features, arc[ 'polyLeft' ], arc[ 'polyRight' ] ) ) {
					var arcLength : Number = getArcLength( arc[ 'coord' ], zoom, conversionFunction );
					var rightFeatureKey : Object = getFeatureKeyForPolygon( features, arc[ 'polyRight' ] );
					var leftFeatureKey : Object = getFeatureKeyForPolygon( features, arc[ 'polyLeft' ] );
					if ( ! featureBorders[ rightFeatureKey ] )
						featureBorders[ rightFeatureKey ] = { 'neighbors' : {} };
					if ( ! featureBorders[ leftFeatureKey ] )
						featureBorders[ leftFeatureKey ] = { 'neighbors' : {} };
					if ( ! featureBorders[ leftFeatureKey ][ 'neighbors' ][ rightFeatureKey ] )
						featureBorders[ leftFeatureKey ][ 'neighbors' ][ rightFeatureKey ] = 0;
					if ( ! featureBorders[ rightFeatureKey ][ 'neighbors' ][ leftFeatureKey ] )
						featureBorders[ rightFeatureKey ][ 'neighbors' ][ leftFeatureKey ] = 0;
					featureBorders[ leftFeatureKey ][ 'neighbors' ][ rightFeatureKey ] += arcLength;
					featureBorders[ rightFeatureKey ][ 'neighbors' ][ leftFeatureKey ] += arcLength;
				}
				
			}
			
			return featureBorders;
		}
		
		private static function sameFeature( features : Dictionary, polyLeft : int, polyRight : int ) : Boolean
		{
			for each ( var feature : Object in features ) {
				var leftIndex : int = ( feature[ 'polygons' ] as Array ).indexOf( polyLeft-1 );
				var rightIndex : int = ( feature[ 'polygons' ] as Array ).indexOf( polyRight-1 );
				if ( leftIndex != -1 ) {
					return ( rightIndex != -1 );
				} else if ( rightIndex != -1 ) {
					return false;
				}
			}
			return true;
		}
		
		private static function getArcLength( points : Vector.<Point>, zoom : Number = 1, conversionFunction : Function = null ) : Number
		{
			var length : Number = 0;
			var lastPoint : Point = ( conversionFunction == null ) ? points[ 0 ] : conversionFunction( points[ 0 ] );
			lastPoint = new Point( lastPoint.x * zoom, lastPoint.y * zoom );
			for ( var i : int = 1; i < points.length; i++ ) {
				var thisPoint : Point = ( conversionFunction == null ) ? points[ i ] : conversionFunction( points[ i ] );
				thisPoint = new Point( thisPoint.x * zoom, thisPoint.y * zoom );
				length += Point.distance( lastPoint, thisPoint );
				lastPoint = thisPoint;
			}
			return length;
		}
		
		private static function getFeatureKeyForPolygon( features : Dictionary, polyID : int ) : Object
		{
			for ( var key : Object in features ) {
				if ( ( features[ key ][ 'polygons' ] as Array ).indexOf( polyID-1 ) != -1 ) {
					return key;
				}
			}
			return null;
		}
		
		/**
		 * Returns an array of all neighbor keys for each feature. */
		public static function getAllFeatureNeighbors(
			e00Data : Object,
			dataSection : Vector.<Object>,
			attribute : String
		) : Dictionary
		{
			var featureNeighbors : Dictionary = new Dictionary();
			var features : Dictionary = getAllFeaturePolys( dataSection, attribute );
			
			for ( var key : Object in features ) {
				
				var polyNeighbors : Array = [];
				for each ( var polyID : Number in features[ key ][ 'polygons' ] ) {
					for each ( var arc : Object in e00Data[ 'pal' ][ polyID ][ 'points' ] ) {
						if ( polyNeighbors.indexOf( arc[ 'polygon-number' ] ) == -1 ) 
							polyNeighbors.push( arc[ 'polygon-number' ] );
					}
				}
				var neighbors : Array = [];
				
				for each ( var polyNeighbor : Object in polyNeighbors ) {
					
					for ( var key2 : Object in features ) {
						if ( key2 == key ) continue;
						var index : int = ( features[ key2 ][ 'polygons' ] as Array ).indexOf( Number( polyNeighbor )-1 );
						if ( neighbors.indexOf( key2 ) == -1 && index != -1 ) {
							neighbors.push( key2 );
						}
					}
				}
				
				featureNeighbors[ key ] = neighbors;
			}
			
			return featureNeighbors;
		}
		
		/**
		 * Returns a dictionary of feature sprites, with the unique attribute values used as keys. */
		public static function drawAllFeatures( 
			e00Data 			: Object, 
			dataSection 		: Vector.<Object>, 
			attribute 			: String,
			conversionFunction 	: Function = null,
			width 				: Number = NaN,
			height 				: Number = NaN,
			graphicsCallBack	: Function = null,
			featuresToExclude 	: Array = null
		) : Dictionary
		{
			if ( featuresToExclude == null ) featuresToExclude = [];
			var features : Dictionary = getAllFeaturePolys( dataSection, attribute );
			var featureSprites : Dictionary = new Dictionary();
			
			var zoom : Number = 1;
			var xAdjustment : Number = 0;
			var yAdjustment : Number = 0;
			
			if ( !isNaN( width ) && !isNaN( height ) ) {
				//stores bounds for each feature
				var boundsDictionary : Dictionary = new Dictionary();
				
				//overall feature bounds
				var xmax : Number = -Infinity;
				var ymax : Number = -Infinity;
				var xmin : Number = Infinity;
				var ymin : Number = Infinity;
				
				for ( key in features ) {
					if ( featuresToExclude.indexOf( key ) != -1 ) continue;
					boundsDictionary[ key ] = getFeatureBounds( features[ key ][ 'polygons' ], e00Data, conversionFunction );
					
					if ( boundsDictionary[ key ][ 'x' ] < xmin ) xmin = boundsDictionary[ key ][ 'x' ];
					if ( boundsDictionary[ key ][ 'y' ] < ymin ) ymin = boundsDictionary[ key ][ 'y' ];
					if ( boundsDictionary[ key ][ 'width' ] > xmax ) xmax = boundsDictionary[ key ][ 'width' ];
					if ( boundsDictionary[ key ][ 'height' ] > ymax ) ymax = boundsDictionary[ key ][ 'height' ];
				}
				var unscaledWidth : Number = ( xmax - xmin );
				var unscaledHeight : Number = ( ymax - ymin );
				
				zoom = ( Math.min( height/unscaledHeight, width/unscaledWidth ) );
				xAdjustment = ( -xmin );
				yAdjustment = ( -ymax );
			}
			
			for ( var key : Object in features ) {
				if ( featuresToExclude.indexOf( key ) != -1 ) continue;
				var featureSprite : Sprite = new Sprite();
				
				if ( graphicsCallBack == null ) {
					featureSprite.graphics.lineStyle( 0, 0xffffff, 1, false, "none" );
					featureSprite.graphics.beginFill( 0xcccccc, 1 );
				} else {
					graphicsCallBack( featureSprite.graphics );
				}
				
				for each ( var polyID : int in features[ key ][ 'polygons' ] ) {
					drawPolygon( e00Data, polyID, featureSprite.graphics, conversionFunction, zoom );
				}
				
				featureSprite.graphics.endFill();
				var boxy : Rectangle = boundsDictionary[ key ];
				
				featureSprite.x += xAdjustment * zoom;
				featureSprite.y -= yAdjustment * zoom;
				
				featureSprites[ key ] = featureSprite;
			}
			featureSprites[ 'zoom' ] = zoom;
			featureSprites[ 'xAdjustment' ] = xAdjustment;
			featureSprites[ 'yAdjustment' ] = yAdjustment;
			
			return featureSprites;
		}
		
		private static function getFeatureBounds( polygons : Array, e00Data : Object, conversionFunction : Function = null ) : Rectangle
		{
			var bounds : Rectangle = new Rectangle( Infinity, Infinity, -Infinity, -Infinity );
			for each ( var polyID : int in polygons ) {
				var polyBounds : Rectangle = getPolygonBounds( e00Data, polyID, conversionFunction );
				
				if ( polyBounds.x < bounds.x ) bounds.x = polyBounds.x;
				if ( polyBounds.y < bounds.y ) bounds.y = polyBounds.y;
				if ( polyBounds.width > bounds.width ) bounds.width = polyBounds.width;
				if ( polyBounds.height > bounds.height ) bounds.height = polyBounds.height;
			}
			return bounds;
		}
		
		private static function getPolygonBounds( e00Data : Object, polyID : int, conversionFunction : Function = null ) : Rectangle
		{
			var bounds : Rectangle = new Rectangle( Infinity, Infinity, -Infinity, -Infinity );
			for each ( var arc : Object in e00Data[ 'pal' ][ polyID ][ 'points' ] ) {
				var arcNum : int = Math.abs( arc[ 'arc-number' ] );
				if ( arcNum == 0 ) continue;
				for each ( var point : Object in e00Data[ 'arc' ][ arcNum - 1 ][ 'coord' ] ) {
					if ( conversionFunction != null ) point = conversionFunction( point );
					if ( point.x < bounds.x ) bounds.x = point.x;
					if ( point.y < bounds.y ) bounds.y = point.y;
					if ( point.x > bounds.width ) bounds.width = point.x;
					if ( point.y > bounds.height ) bounds.height = point.y;
				}
			}
			return bounds;
		}
		
		/**
		 * Draws a polygon to a target Graphics instance. */
		public static function drawPolygon( 
			e00Data 			: Object, 
			polyID 				: int, 
			target 				: Graphics, 
			conversionFunction 	: Function = null,
			zoom				: int = 1
		) : void
		{
			var moveToNotLineTo : Boolean = true;
			for each ( var arc : Object in e00Data[ 'pal' ][ polyID ][ 'points' ] ) {
				var arcNum : int = Math.abs( arc[ 'arc-number' ] ) - 1;
				if ( arcNum == -1 ) {
					moveToNotLineTo = true;
					continue;
				}
				
				if ( arc[ 'arc-number' ] < 0 ) {
					for ( var pointNum : int = e00Data[ 'arc' ][ arcNum ][ 'coord' ][ 'length' ]-1; pointNum >= 0; pointNum -- ) {
						var point : Object = ( conversionFunction == null ) ? e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] : conversionFunction( e00Data[ 'arc' ][ arcNum ][ 'coord' ][ pointNum ] );
						if ( moveToNotLineTo ) {
							target.moveTo( point.x * zoom, -point.y * zoom );
							moveToNotLineTo = false;
						} else {
							target.lineTo( point.x * zoom, -point.y * zoom );
						}
					}
				} else {
					for each ( point in e00Data[ 'arc' ][ arcNum ][ 'coord' ] ) {
						if ( conversionFunction != null ) point = conversionFunction( point );
						if ( moveToNotLineTo ) {
							target.moveTo( point.x * zoom, -point.y * zoom );
							moveToNotLineTo = false;
						} else {
							target.lineTo( point.x * zoom, -point.y * zoom );
						}
					}
				}				
			}
		}
	} // end of class E00Tools
}